package com.YapaStream.impl;

/*
 * Copyright (C) 2009 The Sipdroid Open Source Project
 * 
 * This file is part of Sipdroid (http://www.sipdroid.org)
 * 
 * Sipdroid is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This source code is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this source code; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.DatagramSocketImpl;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketOptions;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
//import java.security.AccessController;

/**
 * The default, concrete instance of datagram sockets. This class does not
 * support security checks. Alternative types of DatagramSocketImpl's may be
 * used by setting the <code>impl.prefix</code> system property.
 */
public class PlainDatagramSocketImpl extends DatagramSocketImpl {

	static final int MULTICAST_IF = 1;

	static final int MULTICAST_TTL = 2;

	static final int TCP_NODELAY = 4;

	static final int FLAG_SHUTDOWN = 8;

	private final static int SO_BROADCAST = 32;

	final static int IP_MULTICAST_ADD = 19;

	final static int IP_MULTICAST_DROP = 20;

	final static int IP_MULTICAST_TTL = 17;

	/**
	 * for datagram and multicast sockets we have to set REUSEADDR and REUSEPORT
	 * when REUSEADDR is set for other types of sockets we need to just set
	 * REUSEADDR therefore we have this other option which sets both if
	 * supported by the platform. this cannot be in SOCKET_OPTIONS because since
	 * it is a public interface it ends up being public even if it is not
	 * declared public
	 */
	static final int REUSEADDR_AND_REUSEPORT = 10001;

	private boolean bindToDevice;

	private byte[] ipaddress = { 0, 0, 0, 0 };

	private int ttl = 1;

	private OSNetworkSystem netImpl = OSNetworkSystem.getOSNetworkSystem();

	private volatile boolean isNativeConnected;

	public int receiveTimeout;

	public boolean streaming = true;

	public boolean shutdownInput;

	/**
	 * used to keep address to which the socket was connected to at the native
	 * level
	 */
	private InetAddress connectedAddress;

	private int connectedPort = -1;

	/**
	 * used to store the trafficClass value which is simply returned as the
	 * value that was set. We also need it to pass it to methods that specify an
	 * address packets are going to be sent to
	 */
	private int trafficClass;

	public PlainDatagramSocketImpl(FileDescriptor fd, int localPort) {
		super();
		this.fd = fd;
		this.localPort = localPort;
	}

	public PlainDatagramSocketImpl() {
		super();
		fd = new FileDescriptor();
	}

	@Override
	public void bind(int port, InetAddress addr) throws SocketException {
		String prop = null; //AccessController.doPrivileged(new PriviAction<String>("bindToDevice")); //$NON-NLS-1$
		boolean useBindToDevice = prop != null
				&& prop.toLowerCase().equals("true"); //$NON-NLS-1$
		bindToDevice = netImpl.bind2(fd, port, useBindToDevice, addr);
		if (0 != port) {
			localPort = port;
		} else {
			// localPort = netImpl.getSocketLocalPort(fd,
			// NetUtil.preferIPv6Addresses());
		}

		try {
			// Ignore failures
			setOption(SO_BROADCAST, Boolean.TRUE);
		} catch (IOException e) {
		}
	}

	@Override
	public void close() {
		synchronized (fd) {
			if (fd.valid()) {
				try {
					netImpl.socketClose(fd);
				} catch (IOException e) {
				}
				fd = new FileDescriptor();
			}
		}
	}

	@Override
	public void create() throws SocketException {
		netImpl.createDatagramSocket(fd, false); // NetUtil.preferIPv4Stack());
	}

	@Override
	protected void finalize() {
		close();
	}

	public Object getOption(int optID) throws SocketException {
		if (optID == SocketOptions.SO_TIMEOUT) {
			return Integer.valueOf(receiveTimeout);
		} else if (optID == SocketOptions.IP_TOS) {
			return Integer.valueOf(trafficClass);
		} else {
			// Call the native first so there will be
			// an exception if the socket if closed.
			Object result = netImpl.getSocketOption(fd, optID);
			if (optID == SocketOptions.IP_MULTICAST_IF
					&& (netImpl.getSocketFlags() & MULTICAST_IF) != 0) {
				try {
					return InetAddress.getByAddress(ipaddress);
				} catch (UnknownHostException e) {
					return null;
				}
			}
			return result;
		}
	}

	@Override
	public int getTimeToLive() throws IOException {
		// Call the native first so there will be an exception if the socket if
		// closed.
		int result = (((Byte) getOption(IP_MULTICAST_TTL)).byteValue()) & 0xFF;
		if ((netImpl.getSocketFlags() & MULTICAST_TTL) != 0) {
			return ttl;
		}
		return result;
	}

	@Override
	public byte getTTL() throws IOException {
		// Call the native first so there will be an exception if the socket if
		// closed.
		byte result = ((Byte) getOption(IP_MULTICAST_TTL)).byteValue();
		if ((netImpl.getSocketFlags() & MULTICAST_TTL) != 0) {
			return (byte) ttl;
		}
		return result;
	}

	@Override
	public void join(InetAddress addr) throws IOException {
		// setOption(IP_MULTICAST_ADD, new GenericIPMreq(addr));
	}

	@Override
	public void joinGroup(SocketAddress addr, NetworkInterface netInterface)
			throws IOException {
		if (addr instanceof InetSocketAddress) {
			//InetAddress groupAddr = ((InetSocketAddress) addr).getAddress();
			// setOption(IP_MULTICAST_ADD, new GenericIPMreq(groupAddr,
			// netInterface));
		}
	}

	@Override
	public void leave(InetAddress addr) throws IOException {
		// setOption(IP_MULTICAST_DROP, new GenericIPMreq(addr));
	}

	@Override
	public void leaveGroup(SocketAddress addr, NetworkInterface netInterface)
			throws IOException {
		if (addr instanceof InetSocketAddress) {
			//InetAddress groupAddr = ((InetSocketAddress) addr).getAddress();
			// setOption(IP_MULTICAST_DROP, new GenericIPMreq(groupAddr,
			// netInterface));
		}
	}

	@Override
	protected int peek(InetAddress sender) throws IOException {
		if (isNativeConnected) {
			/*
			 * in this case we know the port and address from which the data
			 * must have be been received as the socket is connected. However,
			 * we still need to do the receive in order to know that there was
			 * data received. We use a short buffer as we don't actually need
			 * the packet, only the knowledge that it is there
			 */
			byte[] storageArray = new byte[10];
			DatagramPacket pack = new DatagramPacket(storageArray,
					storageArray.length);
			netImpl.recvConnectedDatagram(fd, pack, pack.getData(),
					pack.getOffset(), pack.getLength(), receiveTimeout, true); // peek
			// to set the sender ,we now use a native function
			// sender.ipaddress = connectedAddress.getAddress();
			netImpl.setInetAddress(sender, connectedAddress.getAddress());
			return connectedPort;
		}
		return netImpl.peekDatagram(fd, sender, receiveTimeout);
	}

	@Override
	public void receive(DatagramPacket pack) throws java.io.IOException {
		try {
			if (isNativeConnected) {
				// do not peek
				netImpl.recvConnectedDatagram(fd, pack, pack.getData(),
						pack.getOffset(), pack.getLength(), receiveTimeout,
						false);
				updatePacketRecvAddress(pack);
			} else {
				// receiveDatagramImpl2
				netImpl.receiveDatagram(fd, pack, pack.getData(),
						pack.getOffset(), pack.getLength(), receiveTimeout,
						false);
			}
		} catch (InterruptedIOException e) {
			throw new SocketTimeoutException(e.getMessage());
		}
	}

	@Override
	public void send(DatagramPacket packet) throws IOException {

		if (isNativeConnected) {
			netImpl.sendConnectedDatagram(fd, packet.getData(),
					packet.getOffset(), packet.getLength(), bindToDevice);
		} else {
			// sendDatagramImpl2
			netImpl.sendDatagram(fd, packet.getData(), packet.getOffset(),
					packet.getLength(), packet.getPort(), bindToDevice,
					trafficClass, packet.getAddress());
		}
	}

	/**
	 * Set the nominated socket option. As the timeouts are not set as options
	 * in the IP stack, the value is stored in an instance field.
	 * 
	 * @throws SocketException
	 *             thrown if the option value is unsupported or invalid
	 */
	public void setOption(int optID, Object val) throws SocketException {
		/*
		 * for datagram sockets on some platforms we have to set both the
		 * REUSEADDR AND REUSEPORT so for REUSEADDR set this option option which
		 * tells the VM to set the two values as appropriate for the platform
		 */
		if (optID == SocketOptions.SO_REUSEADDR) {
			optID = REUSEADDR_AND_REUSEPORT;
		}

		if (optID == SocketOptions.SO_TIMEOUT) {
			receiveTimeout = ((Integer) val).intValue();
		} else {
			int flags = netImpl.getSocketFlags();
			try {
				netImpl.setSocketOption(fd, optID | (flags << 16), val);
			} catch (SocketException e) {
				// we don't throw an exception for IP_TOS even if the platform
				// won't let us set the requested value
				if (optID != SocketOptions.IP_TOS) {
					throw e;
				}
			}
			if (optID == SocketOptions.IP_MULTICAST_IF
					&& (flags & MULTICAST_IF) != 0) {
				InetAddress inet = (InetAddress) val;
				// if (NetUtil.bytesToInt(inet.getAddress(), 0) == 0 ||
				// inet.isLoopbackAddress()) {
				// ipaddress = ((InetAddress) val).getAddress();
				// } else
				{
					InetAddress local = null;
					try {
						local = InetAddress.getLocalHost();
					} catch (UnknownHostException e) {
						throw new SocketException("getLocalHost(): "
								+ e.toString());
					}
					if (inet.equals(local)) {
						ipaddress = ((InetAddress) val).getAddress();
					} else {
						throw new SocketException(val + " != getLocalHost(): "
								+ local);
					}
				}
			}
			/*
			 * save this value as it is actually used differently for IPv4 and
			 * IPv6 so we cannot get the value using the getOption. The option
			 * is actually only set for IPv4 and a masked version of the value
			 * will be set as only a subset of the values are allowed on the
			 * socket. Therefore we need to retain it to return the value that
			 * was set. We also need the value to be passed into a number of
			 * natives so that it can be used properly with IPv6
			 */
			if (optID == SocketOptions.IP_TOS) {
				trafficClass = ((Integer) val).intValue();
			}
		}
	}

	@Override
	public void setTimeToLive(int ttl) throws java.io.IOException {
		setOption(IP_MULTICAST_TTL, Byte.valueOf((byte) (ttl & 0xFF)));
		if ((netImpl.getSocketFlags() & MULTICAST_TTL) != 0) {
			this.ttl = ttl;
		}
	}

	@Override
	public void setTTL(byte ttl) throws java.io.IOException {
		setOption(IP_MULTICAST_TTL, Byte.valueOf(ttl));
		if ((netImpl.getSocketFlags() & MULTICAST_TTL) != 0) {
			this.ttl = ttl;
		}
	}

	@Override
	public void connect(InetAddress inetAddr, int port) throws SocketException {

		// connectDatagram impl2
		netImpl.connectDatagram(fd, port, trafficClass, inetAddr);

		// if we get here then we are connected at the native level
		try {
			connectedAddress = InetAddress.getByAddress(inetAddr.getAddress());
		} catch (UnknownHostException e) {
			// this is never expected to happen as we should not have gotten
			// here if the address is not resolvable
			throw new SocketException("K0317 " + inetAddr.getHostName()); //$NON-NLS-1$
		}
		connectedPort = port;
		isNativeConnected = true;
	}

	@Override
	public void disconnect() {
		try {
			netImpl.disconnectDatagram(fd);
		} catch (Exception e) {
			// there is currently no way to return an error so just eat any
			// exception
		}
		connectedPort = -1;
		connectedAddress = null;
		isNativeConnected = false;
	}

	@Override
	public int peekData(DatagramPacket pack) throws IOException {
		try {
			if (isNativeConnected) {
				netImpl.recvConnectedDatagram(fd, pack, pack.getData(),
						pack.getOffset(), pack.getLength(), receiveTimeout,
						true); // peek
				updatePacketRecvAddress(pack);
			} else {
				// receiveDatagram 2
				netImpl.receiveDatagram(fd, pack, pack.getData(),
						pack.getOffset(), pack.getLength(), receiveTimeout,
						true); // peek
			}
		} catch (InterruptedIOException e) {
			throw new SocketTimeoutException(e.toString());
		}
		return pack.getPort();
	}

	/**
	 * Set the received address and port in the packet. We do this when the
	 * Datagram socket is connected at the native level and the
	 * recvConnnectedDatagramImpl does not update the packet with address from
	 * which the packet was received
	 * 
	 * @param packet
	 *            the packet to be updated
	 */
	private void updatePacketRecvAddress(DatagramPacket packet) {
		packet.setAddress(connectedAddress);
		packet.setPort(connectedPort);
	}

}